/*
 * This game was initially created by Momchil Atanasov as a
 * project for university (Bulgaria, Sofia University,
 * Faculty of Mathematics and Informatics).
 *
 * The idea was to show that a game could just as well
 * be created using Object-Oriented Programming.
 *
 * The mechanics of the game were inspired by an
 * article on the internet about easily simulating object
 * collision.
 * http://www.metanetsoftware.com/technique/tutorialA.html
 *
 * Future developers must follow the following requests:
 *  -Please keep the game as much OOP as possible even when adding
 *      more complicated mechanics and functionality. The idea is
 *      to display the flexibility of OOP programing.
 *  -Please keep my name in the source code where little has been
 *      modified and in the title of the game window.
 *  -Please follow the code conventions of the java language. They can
 *      be found on the internet or use my code as a reference.
 *  -Do your best to write clean and well structured code. Keep it
 *      well commented when needed. If comments get to complicated and
 *      long, consider refactoring / rewriting the code.
 *  -Currently the game has a nice balance and is overall interesting
 *      for play. Do your best not to ruin the nice flow of the game.
 *  -WRITE EVERYHING IN ENGLISH! I am not from an english-speaking
 *      country but I do follow this source code standard. :)
 * 
 * IDEAS:
 *  A brick that makes the slider constantly active
 *  A brick that makes the slider smaller
 *  A brick that when released starts to fall.
 *  A timed brick that must be hit in a predefined time.
 *  A brick that causes the slider to move upwards. It could be
 *      a brick that need not be destroyed in the game in order to win.
 *  A brick that causes the slider to have coprehendable mass and thus each
 *      hit by a ball would cause it to go down.
 *  Could make the slider dynamic (have speed and mass) with infinite mass and one
 *      could use the slider`s side speed to manipulate somehow the ball. If the
 *      more features are added to the collision engine.
 *
 * THINGS THAT COULD BE BETTER:
 *  Brick type initialization is a bit tricky. One does not have control when the
 *      class gets first initialized thus when all bricktypes request their images.
 *
 */

package brickdestroyer;

import brickdestroyer.simulation.Simulation;
import brickdestroyer.simulation.SimulationListener;
import brickdestroyer.simulation.SimulationState;
import brickdestroyer.util.ImageResources;
import java.awt.AWTException;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.PointerInfo;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.MemoryImageSource;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JFrame;
import static brickdestroyer.FrameDecorations.*;
import static brickdestroyer.simulation.Simulation.*;

/**
 * This class is the starting point of the game.
 * Here the GUI is initialized and bound to the game logic.
 * 
 * @author Momchil Atanasov
 */
public final class Main extends JFrame {

    private static final String WINDOW_CAPTION = "Momchil Atanasov's 'Brick Destroyer' Game!";

    private static final int CANVAS_WIDTH = PLAYABLE_AREA_WIDTH + BORDER_LEFT + BORDER_RIGHT;

    private static final int CANVAS_HEIGHT = PLAYABLE_AREA_HEIGHT + BORDER_TOP + BORDER_BOTTOM;

    /*
     * Amount of milliseconds from one update to the next.
     */
    private static final int GAME_TIMER_COOLDOWN = 1000 / SIMULATION_FPS;

    /*
     * Timer used for the frame updates.
     */
    private final Timer frameTimer = new Timer();

    /*
     * Used for mouse event listening
     */
    private final MouseEventHandler mouseListener = new MouseEventHandler();

    /*
     * Used for keyboard event listening
     */
    private final KeyboardEventHandler keyboardListener = new KeyboardEventHandler();

    /*
     * Used for simulation event listening
     */
    private final SimulationHandler simulationListener = new SimulationHandler();

    /*
     * This object holds an interface to the simulation.
     * One must use this object to update the simulation and
     * pass the necessary input.
     */
    private final Simulation simulation = new Simulation();

    /*
     * A way of moving the decoration part of the UI out of this
     * class for better structure and easier future modification.
     */
    private final FrameDecorations decoration = new FrameDecorations(CANVAS_WIDTH, CANVAS_HEIGHT);

    /*
     * The whole scene gets drawn on this canvas. It is the only
     * swing component added to the frame.
     */
    private final Canvas displayCanvas;

    /*
     * Robot object for mouse manipulation - centering.
     * If this object is null then such a robot could
     * not be constructed and the game has to work
     * without centering.
     */
    private final Robot guiRobot;

    private final Cursor defaultCursor = Cursor.getDefaultCursor();

    private final Cursor invisibleCursor = createHiddenCursor();

    /*
     * Determines whether the cursor / mouse is beind
     * centered at the moment. This is used so that mouse move
     * events are not handled when centering.
     */
    private boolean centeringMouse = false;
    
    
    public static void main(String[] args) {
        ImageResources.initialize("/brickdestroyer/data/");
        new Main();
    }

    protected Main() {
        setTitle(WINDOW_CAPTION);
        setIconImage(ImageResources.getImage("ball.png"));
        setResizable(false);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        displayCanvas = new CustomCanvas();
        displayCanvas.setLocation(0, 0);
        displayCanvas.setSize(CANVAS_WIDTH, CANVAS_HEIGHT);
        displayCanvas.setBackground(Color.BLACK);
        displayCanvas.setFont(new Font("Arial", Font.BOLD, 14));
        displayCanvas.setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT));
        displayCanvas.addKeyListener(keyboardListener);
        displayCanvas.addMouseListener(mouseListener);
        displayCanvas.addMouseMotionListener(mouseListener);
        add(displayCanvas);
        pack();
        displayCanvas.requestFocus();
        setLocationRelativeTo(null);
        setVisible(true);
        
        guiRobot = createGUIRobot();
        if (guiRobot!= null) {
            setCursorVisibility(false);
        }

        simulation.addSimulationListener(simulationListener);
        startFrameTimer();
    }

    /*
     * Creates a robot object for manipulating the mouse.
     */
    private static Robot createGUIRobot() {
        Robot result = null;
        try {
            result = new Robot();
        } catch (AWTException ex) {
            System.err.println("Mouse robot not supported.");
            result = null;
        }
        return result;
    }
             
    /*
     * Initializes the frame (also game update) timer.
     */
    private void startFrameTimer() {
        frameTimer.schedule(new FrameTimerTask(), 1, GAME_TIMER_COOLDOWN);
    }

    private void setCursorVisibility(boolean value) {
        Cursor cursor = value ? defaultCursor : invisibleCursor;
        setCursor(cursor);
    }

    /*
     * This method gets called by the timer.
     * It updates the game simulation and redraws it.
     */
    private void onFrameTimer() {
        updateSimulation();
        displaySimulation(displayCanvas.getGraphics());
    }

    /*
     * Causes the whole canvas to get repainted.
     */
    private final void repaintCanvas() {
        Graphics g = displayCanvas.getGraphics();
        decoration.display(g);
//        background.setGameMessage(simulation.getGameMessage());
        displaySimulation(g);
    }

    private void updateSimulation() {
        simulation.update();
    }

    private final void displaySimulation(Graphics g) {
        g.translate(BORDER_LEFT, BORDER_TOP);
        simulation.display(g);
    }

    
    private class FrameTimerTask extends TimerTask {
        public void run() {
            onFrameTimer();
        }
    }

    /*
     * This custom canvas overrides the paint method thus
     * allowing for a custom painting on the component.
     */
    private class CustomCanvas extends Canvas {
        @Override
        public void paint(Graphics g) {
            // Currently the game message gets drawn over the inner border
            // of the canvas so we have to repaint the whole thing.
            repaintCanvas();
        }
    }

    /*
     * This class is used for simulation event handling. That way methods are
     * not exposed pulbicly.
     */
    private class SimulationHandler implements SimulationListener {
        public void onGameMessageChange(String value) {
            decoration.setGameMessage(value);
            repaintCanvas();
        }

        public void onSimulationStateChanged(SimulationState state) {
            switch (state) {
                case START: case RESTART: {
                    decoration.setGameMessage("Level: " + simulation.getLevel());
                    break;
                }
                case END: {
                    decoration.setGameMessage("Game Over!");
                    break;
                }
                case NEXT_LEVEL: {
                    decoration.setGameMessage("Congratulations! Onto the next level: " +
                            simulation.getLevel());
                    break;
                }
            }
            repaintCanvas();
        }
    }

    /*
     * This class is used for mouse event handling. That way methods are not
     * exposed publicly.
     */
    private class MouseEventHandler extends MouseAdapter {
        @Override
        public void mousePressed(MouseEvent e) {
            switch (e.getButton()) {
                /* Left mouse button */
                case MouseEvent.BUTTON1: {
                    simulation.toggleSlider();
                    break;
                }
                /* Right mouse button */
                case MouseEvent.BUTTON3: {
                    simulation.startGame();
                    break;
                }
            }
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            mouseMoved(e);
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            // Do not handle event if it's fake (unwillingly dispatched by us)
            if (centeringMouse) {
                return;
            }
            // Mouse robot may not be present so we have to handle each
            // situation on it's own
            if (guiRobot != null) {
                if (!simulation.isPaused()) {
                    PointerInfo pointerInfo = MouseInfo.getPointerInfo();
                    int delta = pointerInfo.getLocation().x - getCanvasAbsoluteCenterX();
                    simulation.setSliderPosition(simulation.getSliderPosition() + (float)delta);
                    centerMouse();
                }
            } else {
                simulation.setSliderPosition((float)(e.getX() - BORDER_LEFT));
            }
        }
    }

    /*
     * This event centers the mouse in the middle of the canvas
     */
    private void centerMouse() {
        centeringMouse = true;
        guiRobot.mouseMove(getCanvasAbsoluteCenterX(), getCanvasAbsoluteCenterY());
        centeringMouse = false;
    }

    private int getCanvasAbsoluteCenterX() {
        return getX() + displayCanvas.getX() + displayCanvas.getWidth() / 2;
    }

    private int getCanvasAbsoluteCenterY() {
        return getY() + displayCanvas.getY() + displayCanvas.getHeight() / 2;
    }

    /*
     * This class is used for keyboard event handling. That way methods are not
     * exposed publicly.
     */
    private class KeyboardEventHandler extends KeyAdapter {
        @Override
        public void keyReleased(KeyEvent e) {
            switch (e.getKeyCode()) {
                case KeyEvent.VK_ESCAPE: {
                    boolean isPaused = simulation.togglePause();
                    if (guiRobot != null) {
                        setCursorVisibility(isPaused);
                        centerMouse();
                    }
                    break;
                }
                case KeyEvent.VK_F1: {
                    simulation.toggleHelp();
                    boolean isPaused = simulation.isPaused();
                    if (guiRobot != null) {
                        setCursorVisibility(isPaused);
                        centerMouse();
                    }
                    break;
                }
                case KeyEvent.VK_F2: {
                    simulation.restartGame();
                    break;
                }
            }
        }
    }

    /**
     * This method creates a transparent cursor.
     */
    private static Cursor createHiddenCursor() {
        final int CURSOR_SIZE = 16;
        int buffer[] = new int[CURSOR_SIZE * CURSOR_SIZE];
        Image cursorImage = Toolkit.getDefaultToolkit().createImage(
                new MemoryImageSource(CURSOR_SIZE, CURSOR_SIZE, buffer, 0, CURSOR_SIZE));
        return Toolkit.getDefaultToolkit().createCustomCursor(cursorImage, new Point(0, 0),
                "HIDDEN_CURSOR");
    }
}
